﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;

namespace Sugata.SolutionFoldersOrganizer
{
    class SolutionWorker
    {

        private delegate void ProcessHierarchyNode(IVsHierarchy hierarchy, uint itemid, int recursionLevel);

        public readonly ItemData Solution;
        public readonly List<ItemData> Projects = new List<ItemData>();
        public readonly List<ItemData> SolutionFolders = new List<ItemData>();

        public readonly static Guid CSharp2010ProjectGuid = new Guid("fae04ec0-301f-11d3-bf4b-00c04f79efbc");
        public readonly static Guid VisualBasic2010ProjectGuid = new Guid("f184b08f-c81c-45f6-a57f-5abd9991f28f");
        public readonly static Guid FSharpProjectGuid = new Guid("f2a71f9b-5d33-465a-a702-920d77279786");
        public readonly static Guid SolutionFolderGuid = new Guid("6bb5f8f0-4483-11d3-8bcf-00c04f8ec28c");

        public List<Guid> ProjectGuids = new List<Guid>() {CSharp2010ProjectGuid,
                                                           VisualBasic2010ProjectGuid,
                                                           FSharpProjectGuid };

        private readonly IVsSolution solutionService;

        public class ItemData
        {
            public string Name;
            public string Directory;
            public string File;
            public string OptionsFile;
        }

        public SolutionWorker(IVsSolution solutionService)
        {

            this.solutionService = solutionService;

            var nodeProcessor = new ProcessHierarchyNode(ParseNode);

            if (solutionService == null)
                throw new NullReferenceException("Unable to instantiate solution service.");

            var solutionHierarchy = solutionService as IVsHierarchy;

            // Solution
            Solution = new ItemData();
            solutionService.GetSolutionInfo(out Solution.Directory,
                                            out Solution.File,
                                            out Solution.OptionsFile);

            //var projects = new string[0];
            //uint projectsCount = 0;
            //uint projectsCountPointer;
            //solutionService.GetProjectFilesInSolution(0, projectsCount, projects, out projectsCountPointer);

            //IEnumHierarchies projectEnumerator;
            //var ret = solutionService.GetProjectEnum((uint)__VSENUMPROJFLAGS.EPF_ALLINSOLUTION, SolutionFolderGuid, out projectEnumerator);
            //if (ret != VSConstants.S_OK)
            //    throw new ApplicationException("Unable to enumerate solution folders. ret=" + ret);

            //var hierarchy = new IVsHierarchy[1];
            //uint hierarchyCount = 0;
            //while (projectEnumerator.Next(1, hierarchy, out hierarchyCount) == VSConstants.S_OK)
            //{
            //    var hierarchySingle = hierarchy[0];
            //    EnumHierarchyItems(hierarchySingle, VSConstants.VSITEMID_ROOT, 0, true, true, nodeProcessor);
            //}

            // Projects
            EnumHierarchyItems(solutionHierarchy,
                               VSConstants.VSITEMID_ROOT,
                               0, true, true,
                               nodeProcessor);


        }

        /// <summary>
        /// This function diplays the name of the Hierarchy node. This function is passed to the 
        /// Hierarchy enumeration routines to process the current node.
        /// </summary>
        /// <param name="hierarchy">Hierarchy of the current node</param>
        /// <param name="id">Itemid of the current node</param>
        /// <param name="recursionLevel">Depth of recursion in hierarchy enumeration. We add one tab
        /// for each level in the recursion.</param>
        private void ParseNode(IVsHierarchy hierarchy, uint id, int recursionLevel)
        {

            object name;
            hierarchy.GetProperty(id, (int)__VSHPROPID.VSHPROPID_Name, out name);

            object type;
            hierarchy.GetProperty(id, (int)__VSHPROPID.VSHPROPID_TypeName, out type);

            object projectDir;
            hierarchy.GetProperty(id, (int)__VSHPROPID.VSHPROPID_ProjectDir, out projectDir);

            object parent;
            hierarchy.GetProperty(id, (int)__VSHPROPID.VSHPROPID_Parent, out parent);
            var castedParentId = (int)parent;

            object root;
            hierarchy.GetProperty(id, (int)__VSHPROPID.VSHPROPID_Root, out root);

            Guid guid;
            hierarchy.GetGuidProperty(id, (int)__VSHPROPID.VSHPROPID_TypeGuid, out guid);

            var isProject = (!string.IsNullOrEmpty((string)name) &&
                             !string.IsNullOrEmpty((string)type) &&
                             !string.IsNullOrEmpty((string)projectDir));

            if (isProject)
            {

                // Project / Solution

                Projects.Add(new ItemData()
                {
                    Name = (string)name,
                    Directory = (string)projectDir
                });

            }

            if (guid == SolutionFolderGuid)
            {

                // Solution folder
                SolutionFolders.Add(new ItemData()
                {
                    Name = (string)name,
                    Directory = (string)projectDir
                });

            }
#if DEBUG

            Common.OutputString(String.Format("id = {0} name={1} type={2} directory={3} parent={4} root={5} typeGuid={6}",
                                id,
                                name,
                                type,
                                projectDir,
                                castedParentId,
                                root,
                                guid));

#endif

        }

        /// <summary>
        /// Enumerates over the hierarchy items for the given hierarchy traversing into nested hierarchies.
        /// </summary>
        /// <param name="hierarchy">hierarchy to enmerate over.</param>
        /// <param name="itemid">item id of the hierarchy</param>
        /// <param name="recursionLevel">Depth of recursion. e.g. if recursion started with the Solution
        /// node, then : Level 0 -- Solution node, Level 1 -- children of Solution, etc.</param>
        /// <param name="hierIsSolution">true if hierarchy is Solution Node. This is needed to special
        /// case the children of the solution to work around a bug with VSHPROPID_FirstChild and 
        /// VSHPROPID_NextSibling implementation of the Solution.</param>
        /// <param name="visibleNodesOnly">true if only nodes visible in the Solution Explorer should
        /// be traversed. false if all project items should be traversed.</param>
        /// <param name="processNodeFunc">pointer to function that should be processed on each
        /// node as it is visited in the depth first enumeration.</param>
        private static void EnumHierarchyItems(IVsHierarchy hierarchy, uint itemid, int recursionLevel, bool hierIsSolution, bool visibleNodesOnly, ProcessHierarchyNode processNodeFunc)
        {
            int hr;
            IntPtr nestedHierarchyObj;
            uint nestedItemId;
            Guid hierGuid = typeof(IVsHierarchy).GUID;

            // Check first if this node has a nested hierarchy. If so, then there really are two 
            // identities for this node: 1. hierarchy/itemid 2. nestedHierarchy/nestedItemId.
            // We will recurse and call EnumHierarchyItems which will display this node using
            // the inner nestedHierarchy/nestedItemId identity.
            hr = hierarchy.GetNestedHierarchy(itemid, ref hierGuid, out nestedHierarchyObj, out nestedItemId);
            if (VSConstants.S_OK == hr && IntPtr.Zero != nestedHierarchyObj)
            {
                IVsHierarchy nestedHierarchy = Marshal.GetObjectForIUnknown(nestedHierarchyObj) as IVsHierarchy;
                Marshal.Release(nestedHierarchyObj);
                // we are responsible to release the refcount on the out IntPtr parameter
                if (nestedHierarchy != null)
                {
                    // Display name and type of the node in the Output Window
                    EnumHierarchyItems(nestedHierarchy, nestedItemId, recursionLevel, false, visibleNodesOnly,
                                       processNodeFunc);
                }
            }
            else
            {
                object pVar;

                // Display name and type of the node in the Output Window
                processNodeFunc(hierarchy, itemid, recursionLevel);

                recursionLevel++;

                //Get the first child node of the current hierarchy being walked
                // NOTE: to work around a bug with the Solution implementation of VSHPROPID_FirstChild,
                // we keep track of the recursion level. If we are asking for the first child under
                // the Solution, we use VSHPROPID_FirstVisibleChild instead of _FirstChild. 
                // In VS 2005 and earlier, the Solution improperly enumerates all nested projects
                // in the Solution (at any depth) as if they are immediate children of the Solution.
                // Its implementation _FirstVisibleChild is correct however, and given that there is
                // not a feature to hide a SolutionFolder or a Project, thus _FirstVisibleChild is 
                // expected to return the identical results as _FirstChild.
                hr = hierarchy.GetProperty(itemid,
                                           ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1)
                                                 ? (int)__VSHPROPID.VSHPROPID_FirstVisibleChild
                                                 : (int)__VSHPROPID.VSHPROPID_FirstChild)),
                                           out pVar);
                Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
                if (VSConstants.S_OK == hr)
                {
                    //We are using Depth first search so at each level we recurse to check if the node has any children
                    // and then look for siblings.
                    uint childId = GetItemId(pVar);
                    while (childId != VSConstants.VSITEMID_NIL)
                    {
                        EnumHierarchyItems(hierarchy, childId, recursionLevel, false, visibleNodesOnly, processNodeFunc);
                        // NOTE: to work around a bug with the Solution implementation of VSHPROPID_NextSibling,
                        // we keep track of the recursion level. If we are asking for the next sibling under
                        // the Solution, we use VSHPROPID_NextVisibleSibling instead of _NextSibling. 
                        // In VS 2005 and earlier, the Solution improperly enumerates all nested projects
                        // in the Solution (at any depth) as if they are immediate children of the Solution.
                        // Its implementation   _NextVisibleSibling is correct however, and given that there is
                        // not a feature to hide a SolutionFolder or a Project, thus _NextVisibleSibling is 
                        // expected to return the identical results as _NextSibling.
                        hr = hierarchy.GetProperty(childId,
                                                   ((visibleNodesOnly || (hierIsSolution && recursionLevel == 1))
                                                        ? (int)__VSHPROPID.VSHPROPID_NextVisibleSibling
                                                        : (int)__VSHPROPID.VSHPROPID_NextSibling),
                                                   out pVar);
                        if (VSConstants.S_OK == hr)
                        {
                            childId = GetItemId(pVar);
                        }
                        else
                        {
                            Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hr);
                            break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the item id.
        /// </summary>
        /// <param name="pvar">VARIANT holding an itemid.</param>
        /// <returns>Item Id of the concerned node</returns>
        private static uint GetItemId(object pvar)
        {
            if (pvar == null) return VSConstants.VSITEMID_NIL;
            if (pvar is int) return (uint)(int)pvar;
            if (pvar is uint) return (uint)pvar;
            if (pvar is short) return (uint)(short)pvar;
            if (pvar is ushort) return (uint)(ushort)pvar;
            if (pvar is long) return (uint)(long)pvar;
            return VSConstants.VSITEMID_NIL;
        }

    }
}
